/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.    
 */
package dk.redillusions.jdbc.proxy;

import java.lang.reflect.Proxy;
import java.sql.Array;
import java.sql.Blob;
import java.sql.CallableStatement;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.NClob;
import java.sql.PreparedStatement;
import java.sql.SQLClientInfoException;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.SQLXML;
import java.sql.Savepoint;
import java.sql.Statement;
import java.sql.Struct;
import java.util.Map;
import java.util.Properties;

import edu.umd.cs.findbugs.annotations.SuppressWarnings;

/**
 * <p>
 * This implementation of the {@link Connection} interface has the
 * responsibility of wrapping returned objects in the correct proxy objects in
 * order to proxy invocations correctly. Methods that don't need to be sent to
 * proxy objects are sent to the underlying {@link Connection}.
 * </p>
 * 
 * @author Sune Wettersteen
 * 
 */
public final class ProxyConnection implements Connection {
    /**
     * The underlying connection used for delegation.
     */
    private final Connection connection;

    /**
     * Constructor.
     * 
     * @param connection
     *            The underlying connection
     */
    public ProxyConnection(final Connection connection) {
        this.connection = connection;
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public void clearWarnings() throws SQLException {
        this.connection.clearWarnings();
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public void close() throws SQLException {
        this.connection.close();
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public void commit() throws SQLException {
        this.connection.commit();
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public Array createArrayOf(final String typeName, final Object[] elements) throws SQLException {
        return this.connection.createArrayOf(typeName, elements);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public Blob createBlob() throws SQLException {
        return this.connection.createBlob();
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public Clob createClob() throws SQLException {
        return this.connection.createClob();
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public NClob createNClob() throws SQLException {
        return this.connection.createNClob();
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public SQLXML createSQLXML() throws SQLException {
        return this.connection.createSQLXML();
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public Statement createStatement() throws SQLException {
        final Statement statement = this.connection.createStatement();

        return this.createProxyStatement(statement);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public Statement createStatement(final int resultSetType, final int resultSetConcurrency, final int resultSetHoldability) throws SQLException {
        final Statement statement = this.connection.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability);

        return this.createProxyStatement(statement);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public Statement createStatement(final int resultSetType, final int resultSetConcurrency) throws SQLException {
        final Statement statement = this.connection.createStatement(resultSetType, resultSetConcurrency);

        return this.createProxyStatement(statement);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public Struct createStruct(final String typeName, final Object[] attributes) throws SQLException {
        return this.connection.createStruct(typeName, attributes);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public boolean getAutoCommit() throws SQLException {
        return this.connection.getAutoCommit();
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public String getCatalog() throws SQLException {
        return this.connection.getCatalog();
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public Properties getClientInfo() throws SQLException {
        return this.connection.getClientInfo();
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public String getClientInfo(final String name) throws SQLException {
        return this.connection.getClientInfo(name);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public int getHoldability() throws SQLException {
        return this.connection.getHoldability();
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public DatabaseMetaData getMetaData() throws SQLException {
        return this.connection.getMetaData();
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public int getTransactionIsolation() throws SQLException {
        return this.connection.getTransactionIsolation();
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public Map<String, Class<?>> getTypeMap() throws SQLException {
        return this.connection.getTypeMap();
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public SQLWarning getWarnings() throws SQLException {
        return this.connection.getWarnings();
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public boolean isClosed() throws SQLException {
        return this.connection.isClosed();
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public boolean isReadOnly() throws SQLException {
        return this.connection.isReadOnly();
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public boolean isValid(final int timeout) throws SQLException {
        return this.connection.isValid(timeout);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public boolean isWrapperFor(final Class<?> iface) throws SQLException {
        return this.connection.isWrapperFor(iface);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public String nativeSQL(final String sql) throws SQLException {
        return this.connection.nativeSQL(sql);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public CallableStatement prepareCall(final String sql, final int resultSetType, final int resultSetConcurrency, final int resultSetHoldability) throws SQLException {
        return this.connection.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public CallableStatement prepareCall(final String sql, final int resultSetType, final int resultSetConcurrency) throws SQLException {
        return this.connection.prepareCall(sql, resultSetType, resultSetConcurrency);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public CallableStatement prepareCall(final String sql) throws SQLException {
        return this.connection.prepareCall(sql);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	@SuppressWarnings(value = "SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING")
    public PreparedStatement prepareStatement(final String sql, final int resultSetType, final int resultSetConcurrency, final int resultSetHoldability) throws SQLException {
        final PreparedStatement preparedStatement = this.connection.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability);

        return this.createProxyPreparedStatement(preparedStatement);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	@SuppressWarnings(value = "SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING")
    public PreparedStatement prepareStatement(final String sql, final int resultSetType, final int resultSetConcurrency) throws SQLException {
        final PreparedStatement preparedStatement = this.connection.prepareStatement(sql, resultSetType, resultSetConcurrency);

        return this.createProxyPreparedStatement(preparedStatement);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	@SuppressWarnings(value = "SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING")
    public PreparedStatement prepareStatement(final String sql, final int autoGeneratedKeys) throws SQLException {
        final PreparedStatement preparedStatement = this.connection.prepareStatement(sql, autoGeneratedKeys);

        return this.createProxyPreparedStatement(preparedStatement);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	@SuppressWarnings(value = "SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING")
    public PreparedStatement prepareStatement(final String sql, final int[] columnIndexes) throws SQLException {
        final PreparedStatement preparedStatement = this.connection.prepareStatement(sql, columnIndexes);

        return this.createProxyPreparedStatement(preparedStatement);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	@SuppressWarnings(value = "SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING")
    public PreparedStatement prepareStatement(final String sql, final String[] columnNames) throws SQLException {
        final PreparedStatement preparedStatement = this.connection.prepareStatement(sql, columnNames);

        return this.createProxyPreparedStatement(preparedStatement);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	@SuppressWarnings(value = "SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING")
    public PreparedStatement prepareStatement(final String sql) throws SQLException {
        final PreparedStatement preparedStatement = this.connection.prepareStatement(sql);

        return this.createProxyPreparedStatement(preparedStatement);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public void releaseSavepoint(final Savepoint savepoint) throws SQLException {
        this.connection.releaseSavepoint(savepoint);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public void rollback() throws SQLException {
        this.connection.rollback();
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public void rollback(final Savepoint savepoint) throws SQLException {
        this.connection.rollback(savepoint);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public void setAutoCommit(final boolean autoCommit) throws SQLException {
        this.connection.setAutoCommit(autoCommit);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public void setCatalog(final String catalog) throws SQLException {
        this.connection.setCatalog(catalog);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public void setClientInfo(final Properties properties) throws SQLClientInfoException {
        this.connection.setClientInfo(properties);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public void setClientInfo(final String name, final String value) throws SQLClientInfoException {
        this.connection.setClientInfo(name, value);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public void setHoldability(final int holdability) throws SQLException {
        this.connection.setHoldability(holdability);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public void setReadOnly(final boolean readOnly) throws SQLException {
        this.connection.setReadOnly(readOnly);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public Savepoint setSavepoint() throws SQLException {
        return this.connection.setSavepoint();
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public Savepoint setSavepoint(final String name) throws SQLException {
        return this.connection.setSavepoint(name);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public void setTransactionIsolation(final int level) throws SQLException {
        this.connection.setTransactionIsolation(level);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public void setTypeMap(final Map<String, Class<?>> map) throws SQLException {
        this.connection.setTypeMap(map);
    }

    /**
     * {@inheritDoc}
     */
    @Override
	public <T> T unwrap(final Class<T> iface) throws SQLException {
        return this.connection.unwrap(iface);
    }

    /**
     * Create the statement proxy which wraps the real statement.
     * 
     * @param statement
     *            The original statement provided by the connection
     * @return The proxy statement or null if the passed statement is null
     */
    private Statement createProxyStatement(final Statement statement) {
        if (statement == null) {
            return null;
        }

        // Create the invocation handler for the proxy
        final StatementInvocationHandler statementInvocationHandler = new StatementInvocationHandler(statement);
        
        for (JDBCProvider provider : JDBCProviderRegistry.getRegistryList()) {
            // Add delegate
            statementInvocationHandler.addStatement(provider.provideStatement());
        }

        // Create and return the statement proxy
        final Statement statementProxy = (Statement) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class<?>[] { Statement.class }, statementInvocationHandler);

        return statementProxy;
    }

    /**
     * Create the prepared statement proxy which wraps the real prepared
     * statement.
     * 
     * @param preparedStatement
     *            The original prepared statement provided by the connection
     * @return The proxy prepared statement or null if the passed prepared
     *         statement is null
     */
    private PreparedStatement createProxyPreparedStatement(final PreparedStatement preparedStatement) {
        if (preparedStatement == null) {
            return null;
        }

        // Create the invocation handler for the proxy
        final PreparedStatementInvocationHandler preparedStatementInvocationHandler = new PreparedStatementInvocationHandler(preparedStatement);

        for (JDBCProvider provider : JDBCProviderRegistry.getRegistryList()) {
        	// Add delegate
        	preparedStatementInvocationHandler.addPreparedStatementDelegate(provider.providePreparedStatement());
        }

        // Create and return the statement proxy
        final PreparedStatement statementProxy = (PreparedStatement) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class<?>[] { PreparedStatement.class }, preparedStatementInvocationHandler);

        return statementProxy;
    }
}
